// Project: Salient
// http://salient.codeplex.com
// 
// Copyright 2010, Sky Sanders <sky at skysanders.net>
// Dual licensed under the MIT or GPL Version 2 licenses.
// http://salient.codeplex.com/license
// 
// Date: April 25 2010 

#region

using System;
using System.Configuration;
using System.Configuration.Provider;
using System.Data.EntityClient;
using System.Data.SQLite;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Hosting;

#endregion

namespace Salient.Web.DataAccess
{
    /// <summary>
    /// 
    /// TODO: document |CURRENTDIRECTORY| token
    /// TODO: apparently System.Data.SQLite can now handle |DATADIRECTORY| and relative paths.
    /// take that into conderation on next refactor.
    /// </summary>
    public sealed class SQLiteEntityConnectionHolder : IDisposable
    {
        #region Constants

        private const string SStrUpperDataDirWithToken = "|DATADIRECTORY|";

        private const string SStrUpperTargetDirWithToken = "|CURRENTDIRECTORY|";

        #endregion

        #region Readonly

        private static readonly object Lock = new object();

        #endregion

        #region Fields

        private readonly EntityConnection _connection;

        private bool _disposed;

        private readonly bool _keepStoreConnectionOpen;

        private bool _opened;

        private readonly SQLiteConnection _storeConnection;

        #endregion

        #region Constructors

        /// <summary>
        /// </summary>
        /// <param name="connectionString"></param>
        /// <param name="keepStoreConnectionOpen"></param>
        public SQLiteEntityConnectionHolder(string connectionString, bool keepStoreConnectionOpen)
        {
            try
            {
                _connection = new EntityConnection(connectionString);
                _storeConnection = (SQLiteConnection)_connection.StoreConnection;
                _storeConnection.ConnectionString = EnsureDbFile(_storeConnection.ConnectionString);
            }
            catch (ArgumentException e)
            {
                throw new ArgumentException(SR.GetString(SR.SqlErrorConnectionString), "connectionString", e);
            }

            _keepStoreConnectionOpen = keepStoreConnectionOpen;

            if (_keepStoreConnectionOpen)
            {
                StoreConnection.Open();
            }
        }

        /// <summary>
        /// </summary>
        /// <param name="connectionString"></param>
        public SQLiteEntityConnectionHolder(string connectionString)
            : this(connectionString, true)
        {
        }

        #endregion

        #region Properties

        /// <summary>
        /// </summary>
        public EntityConnection Connection
        {
            get
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException("Connection");
                }
                return _connection;
            }
        }

        /// <summary>
        /// </summary>
        public bool IsOpen
        {
            get { return _opened; }
        }

        ///<summary>
        ///</summary>
        public bool KeepStoreConnectionOpen
        {
            get { return _keepStoreConnectionOpen; }
        }

        /// <summary>
        /// </summary>
        public SQLiteConnection StoreConnection
        {
            get
            {
                if (_disposed)
                {
                    throw new ObjectDisposedException("StoreConnection");
                }
                return _storeConnection;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// </summary>
        public void Close()
        {
            if (_disposed || !_opened)
            {
                _opened = false;
                return;
            }
            _connection.Close();
        }

        /// <summary>
        /// </summary>
        public void Dispose()
        {
            if (!_disposed)
            {
                Close();
                _disposed = true;
                _connection.Dispose();
            }
        }

        /// <summary>
        /// </summary>
        /// <param name="context"></param>
        /// <param name="revertImpersonate"></param>
        public void Open(HttpContext context, bool revertImpersonate)
        {
            if (_disposed)
            {
                throw new ObjectDisposedException("Connection");
            }

            if (_opened)
            {
                return;
            }

            if (revertImpersonate)
            {
                bool retry=false;
                using (HostingEnvironment.Impersonate())
                {

                    try
                    {
                        _connection.Open();
                    }
                    catch (NotSupportedException)
                    {
                        // this happens in tests?
                        retry = true;
                    }
                }
                if(retry )
                {
                    _connection.Open();
                }
            }
            else
            {
                _connection.Open();
            }
            _opened = true;
        }


        /// <summary>
        /// TODO: document |CURRENTDIRECTORY| token
        /// </summary>
        /// <param name="entityConnectionString"></param>
        /// <param name="revertImpersonation"></param>
        /// <param name="keepStoreConnectionOpen"></param>
        /// <returns></returns>
        public static SQLiteEntityConnectionHolder GetConnection(string entityConnectionString, bool revertImpersonation, bool keepStoreConnectionOpen)
        {
            string connectionString = GetConnectionString(entityConnectionString);
            SQLiteEntityConnectionHolder holder = new SQLiteEntityConnectionHolder(connectionString, keepStoreConnectionOpen);

            bool shouldClose = true;

            try
            {
                holder.Open(null, revertImpersonation);
                shouldClose = false;
            }
            finally
            {
                if (shouldClose)
                {
                    holder.Close();
                    holder = null;
                }
            }
            return holder;
        }

        /// <summary>
        /// TODO: document |CURRENTDIRECTORY| token
        /// </summary>
        /// <param name="entityConnectionString"></param>
        /// <param name="revertImpersonation"></param>
        /// <returns></returns>
        public static SQLiteEntityConnectionHolder GetConnection(string entityConnectionString, bool revertImpersonation)
        {
            return GetConnection(entityConnectionString, revertImpersonation, true);
        }

        /// <summary>
        /// TODO: document |CURRENTDIRECTORY| token
        /// </summary>
        /// <param name="entityConnectionString"></param>
        /// <returns></returns>
        public static SQLiteEntityConnectionHolder GetConnection(string entityConnectionString)
        {
            return GetConnection(entityConnectionString, true);
        }
        #endregion

        #region Private Methods

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sqliteConnectionString"></param>
        public static string EnsureDbFile(string sqliteConnectionString)
        {
            string normalizedConnectionString = NormalizeConnectionString(sqliteConnectionString);

            SQLiteConnectionStringBuilder csb = new SQLiteConnectionStringBuilder(normalizedConnectionString);

            string path = csb.DataSource;

            if (!File.Exists(path))
            {
                MethodInfo pinfo = typeof(HttpRuntime).GetMethod("HasAspNetHostingPermission",
                                                                  BindingFlags.NonPublic | BindingFlags.Static);
                bool hasAspNetHostingPermission =
                    (bool)pinfo.Invoke(null, new object[] { AspNetHostingPermissionLevel.High });

                if (!hasAspNetHostingPermission)
                {
                    throw new ProviderException(SR.ProviderCanNotCreateFileInThisTrustLevel);
                }

                Type t =
                    typeof(RequestNotification).Assembly.GetType("System.Web.ApplicationImpersonationContext", true,
                                                                  false);

                ConstructorInfo ctor =
                    t.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null);

                using ((IDisposable)ctor.Invoke(null))
                {
                    lock (Lock)
                    {
                        HttpContext current = HttpContext.Current;
                        try
                        {
                            string dataDir = Path.GetDirectoryName(path);
                            if (!Directory.Exists(dataDir))
                            {
                                Directory.CreateDirectory(dataDir);

                                try
                                {
                                    if (current != null)
                                    {
                                        MethodInfo minfo =
                                            typeof(HttpRuntime).GetMethod("RestrictIISFolders",
                                                                           BindingFlags.Static |
                                                                           BindingFlags.NonPublic);

                                        minfo.Invoke(null, new object[] { current });
                                    }
                                }
// ReSharper disable EmptyGeneralCatchClause
                                catch
// ReSharper restore EmptyGeneralCatchClause
                                {
                                }
                            }

                            string sql = null;
                            Assembly asm = typeof(SQLiteEntityConnectionHolder).Assembly;
                            using (
                                Stream stream = asm.GetManifestResourceStream("Salient.Web.SQLite.Data.ASPNET_DB_SQLITE.sql"))
                            {
                                if (stream != null)
                                    using (StreamReader reader = new StreamReader(stream))
                                    {
                                        sql = reader.ReadToEnd();
                                    }
                            }
                            if (string.IsNullOrEmpty(sql))
                            {
                                throw new Exception("Could not extract setup script");
                            }

                            csb.FailIfMissing = false;
                            using (SQLiteConnection connection = new SQLiteConnection(csb.ConnectionString))
                            {
                                using (SQLiteCommand cmd = connection.CreateCommand())
                                {
                                    cmd.CommandText = sql;
                                    connection.Open();
                                    cmd.ExecuteNonQuery();
                                }
                            }
                            csb.FailIfMissing = true;
                        }
                        catch (Exception exception)
                        {
                            if ((current == null) || current.IsCustomErrorEnabled)
                            {
                                throw;
                            }
                            HttpException exception2 = new HttpException(exception.Message, exception);
                            //if (exception is UnauthorizedAccessException)
                            //{
                            //    exception2.SetFormatter(new SqlExpressConnectionErrorFormatter(flag ? DataConnectionErrorEnum.CanNotCreateDataDir : DataConnectionErrorEnum.CanNotWriteToDataDir));
                            //}
                            //else
                            //{
                            //    exception2.SetFormatter(new SqlExpressDBFileAutoCreationErrorFormatter(exception));
                            //}
                            throw exception2;
                        }
                    }
                }
            }

            return csb.ConnectionString;
        }

        /// <summary>
        /// </summary>
        /// <returns></returns>
        [PermissionSet(SecurityAction.Assert, Unrestricted = true)]
        private static string GetDataDirectory()
        {
            if (HostingEnvironment.IsHosted)
            {
                return Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data");
            }

            string data = AppDomain.CurrentDomain.GetData("DataDirectory") as string;

            if (string.IsNullOrEmpty(data))
            {
                string directoryName = null;
                ProcessModule module = Process.GetCurrentProcess().MainModule;

                string filename = (module != null) ? module.FileName : null;

                if (!string.IsNullOrEmpty(filename))
                {
                    directoryName = Path.GetDirectoryName(filename);
                }

                if (string.IsNullOrEmpty(directoryName))
                {
                    directoryName = Environment.CurrentDirectory;
                }

                data = Path.Combine(directoryName, "App_Data");

                AppDomain.CurrentDomain.SetData("DataDirectory", data,
                                                new FileIOPermission(FileIOPermissionAccess.PathDiscovery, data));
            }
            return data;
        }

        ///<summary>
        ///</summary>
        ///<param name="sqliteConnectionString"></param>
        ///<returns></returns>
        public static string NormalizeConnectionString(string sqliteConnectionString)
        {
            SQLiteConnectionStringBuilder csb = new SQLiteConnectionStringBuilder(sqliteConnectionString);

            string dataDirectory = null;
            string pattern = null;
            if (Regex.Match(csb.DataSource, Regex.Escape(SStrUpperTargetDirWithToken), RegexOptions.IgnoreCase).Success)
            {
                dataDirectory = Environment.CurrentDirectory;
                pattern = SStrUpperTargetDirWithToken;
            }
            else if (
                Regex.Match(csb.DataSource, Regex.Escape(SStrUpperDataDirWithToken), RegexOptions.IgnoreCase).Success)
            {
                dataDirectory = GetDataDirectory();
                pattern = SStrUpperDataDirWithToken;
            }

            if (!string.IsNullOrEmpty(dataDirectory))
            {
                csb.DataSource =
                    Regex.Replace(csb.DataSource, Regex.Escape(pattern), dataDirectory + "\\", RegexOptions.IgnoreCase);
            }

            return csb.ConnectionString;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="specifiedConnectionString"></param>
        /// <returns></returns>
        private static string GetConnectionString(string specifiedConnectionString)
        {
            if (string.IsNullOrEmpty(specifiedConnectionString))
            {
                return null;
            }

            ConnectionStringSettings cstring =
                ConfigurationManager.ConnectionStrings[specifiedConnectionString];

            return cstring == null ? specifiedConnectionString : cstring.ConnectionString;
        }

        #endregion

    }
    
}